import { DynamicModule, Global, Module, OnApplicationShutdown, Provider, Type } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { PrismaModuleAsyncOptions, PrismaModuleOptions, PrismaOptionsFactory } from './prisma-options.interface';
import { PrismaClient } from '@prisma/client';
import { PRISMA_CONNECTION, PRISMA_CONNECTION_NAME, PRISMA_MODULE_OPTIONS } from './prisma.constants';
import { catchError, defer, lastValueFrom } from 'rxjs';
import { getDBType, handleRetry } from './prisma.utils';



@Module({})
@Global()
export class PrismaCoreModule implements OnApplicationShutdown {

    private static connections: Record<string, any> = {}

    onApplicationShutdown(signal?: string) {
        // throw new Error('Method not implemented.');
        if (PrismaCoreModule.connections && Object.keys(PrismaCoreModule.connections).length > 0) {
            for (const key of Object.keys(PrismaCoreModule.connections)) {
                const connection = PrismaCoreModule.connections[key]
                if (connection && typeof connection.$disconnect == 'function') {
                    connection.$disconnect()
                }
            }
        }
    }


    static forRoot(_options: PrismaModuleOptions) {
        const {
            url,
            options = {},
            name,
            retryAttempts = 10,
            retryDelay = 3000,
            conectionErrorFactory,
            conectionFactory,
        } = _options
        let newOptions = {
            datasourceUrl: url
        }
        if (!Object.keys(options).length) {
            newOptions = { ...newOptions, ...options }
        }

        const dbTyoe = getDBType(url)
        let _prismaClient
        if (dbTyoe == 'mysql') {
            _prismaClient = PrismaClient
        }


        const providerName = name || PRISMA_CONNECTION_NAME

        const prismaConnectionErrorFactory = conectionErrorFactory || ((err) => err)
        const prismaConnectionFactory = conectionFactory || (async (clientOptions) => await new _prismaClient(clientOptions))

        const prismaClientProvider: Provider = {
            provide: providerName,
            useFactory: async () => {
                // 加入错误重试
                const client = await prismaConnectionFactory(newOptions, _prismaClient)
                return lastValueFrom(
                    defer(async () => await client.$connect()).pipe(
                        handleRetry(retryAttempts, retryDelay),
                        catchError((err: any) => {
                            throw prismaConnectionErrorFactory(err)
                        })
                    )
                ).then(() => client)
            },
        }

        const connectionsProvider = {
            provide: PRISMA_CONNECTION,
            useValue: this.connections
        }

        return {
            module: PrismaCoreModule,
            providers: [
                prismaClientProvider,
                connectionsProvider
            ],
            exports: [
                prismaClientProvider,
                connectionsProvider
            ]
        }
    }

    static forRootAsync(_options: PrismaModuleAsyncOptions): DynamicModule {

        const providerName = _options.name || PRISMA_CONNECTION_NAME

        const prismaClientProvider: Provider = {
            provide: providerName,
            useFactory: async (prismaModuleOptions: PrismaModuleOptions) => {
                if(!prismaModuleOptions) return;
                
                const {
                    url,
                    options = {},
                    retryAttempts = 10,
                    retryDelay = 3000,
                    conectionErrorFactory,
                    conectionFactory,
                } = prismaModuleOptions

                let newOptions = {
                    datasourceUrl: url
                }
                if (!Object.keys(options).length) {
                    newOptions = { ...newOptions, ...options }
                }

                const dbTyoe = getDBType(url)
                let _prismaClient
                if (dbTyoe == 'mysql') {
                    _prismaClient = PrismaClient
                }

                const prismaConnectionFactory = conectionFactory || (async (clientOptions) => await new _prismaClient(clientOptions))
                const prismaConnectionErrorFactory = conectionErrorFactory || ((err) => err)

                return lastValueFrom(
                    defer(async () => {
                        const url = newOptions.datasourceUrl!
                        if (this.connections[url]) {
                            return this.connections[url]
                        }
                        // 加入错误重试
                        const client = await prismaConnectionFactory(newOptions, _prismaClient)
                        this.connections[url] = client
                        return client
                    }).pipe(
                        handleRetry(retryAttempts, retryDelay),
                        catchError((err: any) => {
                            throw prismaConnectionErrorFactory(err)
                        })
                    )
                )
            },
            inject: [PRISMA_MODULE_OPTIONS]
        }

        const asyncProviders = this.createAsyncProviders(_options)
        const connectionsProvider = {
            provide: PRISMA_CONNECTION,
            useValue: this.connections
        }

        return {
            module: PrismaCoreModule,
            providers: [
                ...asyncProviders,
                prismaClientProvider,
                connectionsProvider
            ],
            exports: [prismaClientProvider, connectionsProvider]
        }
    }

    private static createAsyncProviders(options: PrismaModuleAsyncOptions) {
        if (options.useExisting || options.useExisting) {
            return [this.createAsyncOptionsProviders(options)]
        }

        const useClass = options.useClass as Type<PrismaOptionsFactory>

        return [
            this.createAsyncOptionsProviders(options),
            {
                provide: useClass,
                useClass
            }
        ]
    }

    // 创建PRISMA_MODULE_OPTIONS的provide来源
    private static createAsyncOptionsProviders(options: PrismaModuleAsyncOptions) {
        if (options.useFactory) {
            return {
                provide: PRISMA_MODULE_OPTIONS,
                useFactory: options.useFactory,
                inject: options?.inject || []
            }
        }

        const inject = [
            (options.useClass || options.useExisting) as Type<PrismaOptionsFactory>,
        ]

        return {
            provide: PRISMA_MODULE_OPTIONS,
            inject,
            useFactory: async (optionsFactory: PrismaOptionsFactory) => optionsFactory.createPrismaModuleOptions(),
        }
    }
}
